package app

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/astaxie/beego"
	"github.com/opensourceways/server-common-lib/utils"
	"github.com/sirupsen/logrus"
	"k8s.io/apimachinery/pkg/util/sets"

	sdk "github.com/opensourceways/go-gitee/gitee"

	"cvevulner/cve-ddd/domain"
	"cvevulner/cve-ddd/domain/backend"
	"cvevulner/cve-ddd/domain/latestrpm"
	"cvevulner/cve-ddd/domain/majun"
	"cvevulner/cve-ddd/domain/obs"
	"cvevulner/cve-ddd/domain/repository"
	"cvevulner/cve-ddd/domain/updateinfo"
)

const (
	defaultOwner = "src-openeuler"
	mergedState  = "merged"
)

type ColdPatchService interface {
	CollectCveData(CmdToCollectData)
}

func NewColdPatchService(
	rpm latestrpm.LatestRpm,
	repo repository.CveRepository,
	backend backend.Backend,
	u updateinfo.UpdateInfo,
	o obs.OBS,
	m majun.Majun,
	l *logrus.Entry,
) *coldPatchService {
	service := &coldPatchService{
		obs:        o,
		rpm:        rpm,
		repo:       repo,
		maJun:      m,
		backend:    backend,
		updateInfo: u,
		log:        l,
		giteeToken: beego.AppConfig.String("gitee::git_token"),
	}

	service.initReleaseDate()

	return service
}

type coldPatchService struct {
	obs        obs.OBS
	rpm        latestrpm.LatestRpm
	repo       repository.CveRepository
	maJun      majun.Majun
	backend    backend.Backend
	updateInfo updateinfo.UpdateInfo

	collectLock sync.Mutex
	log         *logrus.Entry

	giteeToken  string
	releaseDate sync.Map
}

// CollectCveData 数据收集接口背景：
// 1.由于数据收集任务十分耗时，只能采用异步回调的方式给调用方数据
// 2.调用方只能按照分支分次调用，但对于收集数据逻辑来说，单个分支仍需要遍历所有数据，为了提高效率，采用一次请求全量收集的方式
// 3.调用方的请求不是串行的，可能在首次收集任务未执行完时，另外的请求就到来，因此将请求信息落库，收集完成之后统一进行处理
// 4.调用方通过CallbackId来关联分支，因此也需要写入数据库进行记录
func (c *coldPatchService) CollectCveData(cmd CmdToCollectData) {
	callback := domain.Callback{
		Date:       cmd.Date,
		Branch:     cmd.Branch,
		CallbackId: cmd.CallbackId,
	}
	callback.SetStatusProcessing()

	// 只记录错误日志，通过数据库唯一索引约束，避免重复任务
	if err := c.repo.AddCallback(callback); err != nil {
		c.log.Errorf("add callback failed: %v", err)
	}

	go func() {
		c.collectLock.Lock() // 防止全量收集逻辑被重复执行
		defer c.collectLock.Unlock()

		defer func() {
			if r := recover(); r != nil {
				c.log.Errorf("handle collect panic: %v", r)
			}
		}()

		_, err := c.repo.FindCollectResult("", cmd.Date, time.Now().Add(-time.Minute*30))
		if err != nil {
			if err = c.generateCollectResult(cmd.Date); err != nil {
				c.log.Errorf("generate collect result failed: %v", err)

				return
			}
		}

		if err = c.handleAllCollectData(); err != nil {
			c.log.Errorf("handle all callback failed: %v", err)
		}
	}()
}

func (c *coldPatchService) generateCollectResult(date string) error {
	dataGroupByBranch, err := c.collectAllData()
	if err != nil {
		return fmt.Errorf("collect all data failed: %w", err)
	}

	// 生成表格并上传obs，方便相关人员查看收集的数据明细（错误只记录，不影响主要逻辑）
	excelData, err := c.updateInfo.GenerateCollectExcel(dataGroupByBranch)
	if err != nil {
		c.log.Errorf("generate excel data failed: %v", err)
	} else {
		if err = c.obs.Upload(c.generateCollectFilePath(), excelData); err != nil {
			c.log.Errorf("upload excel to obs failed: %v", err)
		}
	}

	// 按分支保存数据库，待后续统一执行回调
	for branch, data := range dataGroupByBranch {
		dto := domain.ToCallbackDTO(data)
		result, err := json.Marshal(dto) // 数据都是整体使用，且只使用一次，直接序列化后入库，简化逻辑
		if err != nil {
			c.log.Errorf("marshal callback dto failed: %v", err)

			continue
		}

		cr := domain.CollectResult{
			Branch: branch,
			Date:   date,
			Result: string(result),
		}

		if err := c.repo.SaveCollectResult(cr); err != nil {
			c.log.Errorf("save collect result failed: %v", err)
		}
	}

	return nil
}

func (c *coldPatchService) handleAllCollectData() error {
	callbacks, err := c.repo.GetProcessingCallback()
	if err != nil {
		return err
	}

	for _, callback := range callbacks {
		result, err1 := c.repo.FindCollectResult(callback.Branch, callback.Date, time.Now().Add(-time.Minute*30))
		if err1 != nil {
			c.log.Errorf("find calback result failed: %v", err1)
		}

		if err1 = c.maJun.CollectCallback(callback.CallbackId, result); err1 != nil {
			c.log.Errorf("collect callback failed: %v", err1)
		}

		callback.SetStatusProcessed()
		if err1 = c.repo.UpdateCallback(callback); err1 != nil {
			c.log.Errorf("update callback failed: %v", err1)
		}
	}

	return nil
}

func (c *coldPatchService) collectAllData() (map[string]domain.CollectedDataSlice, error) {
	handleBranch, err := c.maJun.GetReleasedBranch()
	if err != nil {
		return nil, fmt.Errorf("get release branch err: %w", err)
	}

	issueData, err := c.repo.GetAllIssue()
	if err != nil {
		return nil, fmt.Errorf("get all issue data err: %w", err)
	}

	allPackage, err := c.repo.GetAllPackage()
	if err != nil {
		return nil, fmt.Errorf("get all package err: %w", err)
	}

	if err = c.rpm.InitData(handleBranch); err != nil {
		return nil, fmt.Errorf("init rpm data failed:%w", err)
	}

	// filter data concurrently
	concurrencyInstance := NewConcurrency(c, issueData, sets.New(allPackage...), sets.New(handleBranch...))
	filteredData := concurrencyInstance.handleFilterData()

	return filteredData.GroupByBranch(handleBranch), nil
}

func (c *coldPatchService) generateCollectFilePath() string {
	dir := beego.AppConfig.String("obs::upload_updateinfo_dir")

	nowStr := time.Now().Format("2006-01-02-15-04-05")

	return fmt.Sprintf("%s%s-new/collect.xlsx", dir, nowStr)
}

// 过滤issue的条件主要有6个，代码中注释已列出
func (c *coldPatchService) filterData(
	data *domain.CollectedData,
	handleBranchSets,
	packageSets sets.Set[string],
) (bool, error) {
	if len(data.AffectedProduct) == 0 {
		return true, nil
	}
	// 1.issue受影响分支与处理分支必须有交集才处理
	//（受影响分支来自issue的开发人员填写，issue信息经webhook回调时，会更新至cve_security_notice表）
	intersection := handleBranchSets.Intersection(data.AffectProductSet())
	if intersection.Len() == 0 {
		return true, nil
	}

	// 2.不在软件包列表（定时更新的openeuler用到的软件包）的不处理
	if !packageSets.Has(data.Issue.Repo) {
		return true, nil
	}

	prs, _, err := c.getRelatedPR(data.Issue)
	if err != nil {
		return false, fmt.Errorf("get related pr of %s err: %w", data.ToLogString(), err)
	}

	if len(prs) == 0 {
		return true, nil
	}

	needToHandleBranch := make(sets.Set[string])
	for _, pr := range prs {
		if pr.Base.Repo.Namespace.Path != defaultOwner {
			continue
		}

		if pr.State != mergedState {
			continue
		}

		// pr合入的目标分支
		branch := pr.Base.Ref

		// 同1，过滤无关分支
		if !intersection.Has(branch) {
			continue
		}

		mergeAt, err1 := time.ParseInLocation("2006-01-02T15:04:05+08:00", pr.MergedAt, time.Local)
		if err1 != nil {
			continue
		}

		buildTime, err1 := c.rpm.GetBuildTime(branch, data.Issue.Repo)
		if err1 != nil {
			c.log.Errorf("get build time of %s %s error:%v", branch, data.ToLogString(), err1)
			continue
		}

		// 3.pr合入时间必须在工程构建时间之后（pr合入后，工程会重新构建，并上传至latest_rpms仓库，如果工程因其他原因未构建，就不需要转测了）
		if !buildTime.After(mergeAt) {
			c.log.Errorf("build time check failed of %s %s", branch, data.ToLogString())
			continue
		}

		// 4.在版本分支发布日期之前合入的pr不处理，因为已经随着新版本一起发布并修复了
		releaseTimeOfBranch, ok := c.releaseDate.Load(branch)
		if ok {
			releaseTime := releaseTimeOfBranch.(time.Time)
			if releaseTime.After(mergeAt) {
				_ = c.repo.SetIgnoreStatus(data.Id)
				continue
			}
		}

		needToHandleBranch.Insert(branch)
	}

	publishedBranch, err := c.backend.PublishedInfo(data.CveNum, data.Issue.Repo)
	if err != nil {
		err = fmt.Errorf("get published info of %s error:%w", data.ToLogString(), err)

		return false, err
	}

	// 5.已经发布到官网的分支不用再处理了
	publishSets := sets.New(publishedBranch...)
	diff := needToHandleBranch.Difference(publishSets)
	if len(diff) == 0 {
		return true, nil
	}

	data.AffectedProduct = diff.UnsortedList()

	return false, nil
}

func (c *coldPatchService) getRelatedPR(issue domain.Issue) (prs []sdk.PullRequest, code int, err error) {
	endpoint := fmt.Sprintf("https://gitee.com/api/v5/repos/%v/issues/%v/pull_requests?access_token=%s&repo=%s",
		defaultOwner, issue.Number, c.giteeToken, issue.Repo,
	)
	req, err := http.NewRequest(http.MethodGet, endpoint, nil)
	if err != nil {
		return
	}

	cli := utils.NewHttpClient(3)
	bytes, code, err := cli.Download(req)
	if err != nil {
		return
	}

	err = json.Unmarshal(bytes, &prs)

	return
}

func (c *coldPatchService) initReleaseDate() {
	releaseDateConfig := beego.AppConfig.DefaultString("excel::release_date_of_version", "")
	for _, v := range strings.Split(strings.Trim(releaseDateConfig, ";"), ";") {
		split := strings.Split(v, ":")
		key := split[0]
		value, _ := time.ParseInLocation("2006-01-02", split[1], time.Local)
		c.releaseDate.Store(key, value.AddDate(0, 0, 1))
	}
}
